﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;

namespace GameViewer.Ogre
{
    public class OgreMaterialSerializer
    {
        /** \addtogroup Core
            *  @{
            */
        /** \addtogroup Materials
        *  @{
        */
        /** Enum to identify material sections. */
        enum MaterialScriptSection
        {
            MSS_NONE,
            MSS_MATERIAL,
            MSS_TECHNIQUE,
            MSS_PASS,
            MSS_TEXTUREUNIT,
            MSS_PROGRAM_REF,
            MSS_PROGRAM,
            MSS_DEFAULT_PARAMETERS,
            MSS_TEXTURESOURCE
        };
        /** Struct for holding the script context while parsing. */
        struct MaterialScriptContext
        {
            public MaterialScriptSection section;
            public OgreMaterial material;
            public OgreTechnique technique;
            public OgrePass pass;
            public OgreTextureUnitState textureUnit;

            public int techLev,	//Keep track of what tech, pass, and state level we are in
                passLev,
                stateLev;

            // Error reporting state
            public int lineNo;
            public string filename;
        };

        MaterialScriptContext mScriptContext;

        delegate bool AttributeParse(string param, ref MaterialScriptContext content);

        /// Parsers for the root of the material script
        Dictionary<string, AttributeParse> mRootAttribParsers;
        /// Parsers for the material section of a script
        Dictionary<string, AttributeParse> mMaterialAttribParsers;
        /// Parsers for the technique section of a script
        Dictionary<string, AttributeParse> mTechniqueAttribParsers;
        /// Parsers for the pass section of a script
        Dictionary<string, AttributeParse> mPassAttribParsers;
        /// Parsers for the texture unit section of a script
        Dictionary<string, AttributeParse> mTextureUnitAttribParsers;
        /// Parsers for the program reference section of a script
        Dictionary<string, AttributeParse> mProgramRefAttribParsers;
        /// Parsers for the program definition section of a script
        Dictionary<string, AttributeParse> mProgramAttribParsers;
        /// Parsers for the program definition section of a script
        Dictionary<string, AttributeParse> mProgramDefaultParamAttribParsers;



        public OgreMaterialSerializer()
        {
            mRootAttribParsers = new Dictionary<string, AttributeParse>();
            mRootAttribParsers.Add("material", ParseMaterial);
            //mRootAttribParsers.Add("vertex_program", ParseVertexProgram);
            //mRootAttribParsers.Add("geometry_program", ParseGeometryProgram);
            //mRootAttribParsers.Add("fragment_program", ParseFragmentProgram);

            mMaterialAttribParsers = new Dictionary<string, AttributeParse>();
            mMaterialAttribParsers.Add("technique", ParseTechnique);

            mTechniqueAttribParsers = new Dictionary<string, AttributeParse>();
            mTechniqueAttribParsers.Add("pass", ParsePass);

            mPassAttribParsers = new Dictionary<string, AttributeParse>();
            mPassAttribParsers.Add("texture_unit", ParseTextureUnitState);
            mPassAttribParsers.Add("vertex_program_ref", ParseVertexProgramRef);
            mPassAttribParsers.Add("geometry_program_ref", ParseGeometryProgramRef);
            mPassAttribParsers.Add("shadow_caster_vertex_program_ref", ParseShadowCasterVertexProgramRef);
            mPassAttribParsers.Add("shadow_receiver_vertex_program_ref", ParseShadowReceiverVertexProgramRef);
            mPassAttribParsers.Add("shadow_receiver_fragment_program_ref", ParseShadowReceiverFragmentProgramRef);
            mPassAttribParsers.Add("fragment_program_ref", ParseFragmentProgramRef);

            mTextureUnitAttribParsers = new Dictionary<string, AttributeParse>();
            mTextureUnitAttribParsers.Add("texture", ParseTexture);

            mProgramRefAttribParsers = new Dictionary<string, AttributeParse>();

            mProgramAttribParsers = new Dictionary<string, AttributeParse>();

            mProgramDefaultParamAttribParsers = new Dictionary<string, AttributeParse>();
        }

        public void ParseScript(Stream stream, string fileName)
        {
            mScriptContext = new MaterialScriptContext();
            mScriptContext.lineNo = 0;
            mScriptContext.material = null;
            mScriptContext.pass = null;
            mScriptContext.passLev = -1;
            mScriptContext.section = MaterialScriptSection.MSS_NONE;
            mScriptContext.stateLev = -1;
            mScriptContext.techLev = -1;
            mScriptContext.technique = null;
            mScriptContext.textureUnit = null;
            mScriptContext.filename = fileName;

            stream.Position = 0;
            StreamReader reader = new StreamReader(stream);
            string[] lines = reader.ReadToEnd().Split("\n".ToCharArray());
            bool nextIsOpenBrace = false;
            for (int i = 0; i < lines.Length; i++)
            {
                string line = lines[i];
                line = line.Trim();
                mScriptContext.lineNo = i;

                // DEBUG LINE
                // LogManager::getSingleton().logMessage("About to attempt line(#" +
                //    StringConverter::toString(mScriptContext.lineNo) + "): " + line);

                // Ignore comments & blanks
                if (!(line.Length == 0 || line.StartsWith("//")))
                {
                    if (nextIsOpenBrace)
                    {
                        // NB, parser will have changed context already
                        if (line != "{")
                        {
                            logParseError("Expecting '{' but got " +
                                line + " instead.", mScriptContext);
                        }
                        nextIsOpenBrace = false;
                    }
                    else
                    {
                        nextIsOpenBrace = ParseScriptLine(line);
                    }
                }
            }

        }
        //-----------------------------------------------------------------------
        bool ParseVertexProgram(String param, ref MaterialScriptContext context)
        {
            // update section
            context.section = MaterialScriptSection.MSS_PROGRAM;

            //// Create new program definition-in-progress
            //context.programDef = OGRE_NEW_T(MaterialScriptProgramDefinition, MEMCATEGORY_SCRIPTING)();
            //context.programDef->progType = GPT_VERTEX_PROGRAM;
            //context.programDef->supportsSkeletalAnimation = false;
            //context.programDef->supportsMorphAnimation = false;
            //context.programDef->supportsPoseAnimation = 0;
            //context.programDef->usesVertexTextureFetch = false;

            // Get name and language code
            string[] vecparams = param.Split(" \t".ToCharArray());
            if (vecparams.Length != 2)
            {
                logParseError("Invalid vertex_program entry - expected " +
                    "2 parameters.", context);
                return true;
            }
            //// Name, preserve case
            //context.programDef->name = vecparams[0];
            //// language code, make lower case
            //context.programDef->language = vecparams[1];
            //StringUtil::toLowerCase(context.programDef->language);

            // Return TRUE because this must be followed by a {
            return true;
        }
        //-----------------------------------------------------------------------
        bool ParseGeometryProgram(String param, ref MaterialScriptContext context)
        {
            // update section
            context.section = MaterialScriptSection.MSS_PROGRAM;

            //// Create new program definition-in-progress
            //context.programDef = OGRE_NEW_T(MaterialScriptProgramDefinition, MEMCATEGORY_SCRIPTING)();
            //context.programDef->progType = GPT_GEOMETRY_PROGRAM;
            //context.programDef->supportsSkeletalAnimation = false;
            //context.programDef->supportsMorphAnimation = false;
            //context.programDef->supportsPoseAnimation = 0;
            //context.programDef->usesVertexTextureFetch = false;

            // Get name and language code
            string[] vecparams = param.Split(" \t".ToCharArray());
            if (vecparams.Length != 2)
            {
                logParseError("Invalid geometry_program entry - expected " +
                    "2 parameters.", context);
                return true;
            }
            //// Name, preserve case
            //context.programDef->name = vecparams[0];
            //// language code, make lower case
            //context.programDef->language = vecparams[1];
            //StringUtil::toLowerCase(context.programDef->language);

            // Return TRUE because this must be followed by a {
            return true;
        }
        //-----------------------------------------------------------------------
        bool ParseFragmentProgram(String param, ref MaterialScriptContext context)
        {
            // update section
            context.section = MaterialScriptSection.MSS_PROGRAM;

            //// Create new program definition-in-progress
            //context.programDef = OGRE_NEW_T(MaterialScriptProgramDefinition, MEMCATEGORY_SCRIPTING)();
            //context.programDef->progType = GPT_FRAGMENT_PROGRAM;
            //context.programDef->supportsSkeletalAnimation = false;
            //context.programDef->supportsMorphAnimation = false;
            //context.programDef->supportsPoseAnimation = 0;
            //context.programDef->usesVertexTextureFetch = false;

            // Get name and language code
            string[] vecparams = param.Split(" \t".ToCharArray());
            if (vecparams.Length != 2)
            {
                logParseError("Invalid fragment_program entry - expected " +
                    "2 parameters.", context);
                return true;
            }
            //// Name, preserve case
            //context.programDef->name = vecparams[0];
            //// language code, make lower case
            //context.programDef->language = vecparams[1];
            //StringUtil::toLowerCase(context.programDef->language);

            // Return TRUE because this must be followed by a {
            return true;

        }
        //-----------------------------------------------------------------------
        bool ParseTextureCustomParameter(String param, ref MaterialScriptContext context)
        {
            // This params object does not have the command stripped
            // Split only up to first delimiter, program deals with the rest
            string[] vecparams = param.Split(" \t".ToCharArray());
            if (vecparams.Length != 2)
            {
                logParseError("Invalid texture parameter entry; " +
                    "there must be a parameter name and at least one value.",
                    context);
                return false;
            }

            //if(	ExternalTextureSourceManager::getSingleton().getCurrentPlugIn() != 0 )
            //    ////First is command, next could be a string with one or more values
            //    ExternalTextureSourceManager::getSingleton().getCurrentPlugIn()->setParameter( vecparams[0], vecparams[1] );

            return false;
        }
        //-----------------------------------------------------------------------
        bool ParseProgramCustomParameter(String param, ref MaterialScriptContext context)
        {
            // This params object does not have the command stripped
            // Lower case the command, but not the value incase it's relevant
            // Split only up to first delimiter, program deals with the rest
            string[] vecparams = param.Split(" \t".ToCharArray());
            if (vecparams.Length != 2)
            {
                logParseError("Invalid custom program parameter entry; " +
                    "there must be a parameter name and at least one value.",
                    context);
                return false;
            }

            //context.programDef->customParameters.push_back(
            //    std::pair<String, String>(vecparams[0], vecparams[1]));

            return false;
        }
        //-----------------------------------------------------------------------
        bool ParseVertexProgramRef(string param, ref MaterialScriptContext context)
        {
            // update section
            context.section = MaterialScriptSection.MSS_PROGRAM_REF;

            // Return TRUE because this must be followed by a {
            return true;
        }
        //-----------------------------------------------------------------------
        bool ParseGeometryProgramRef(string param, ref MaterialScriptContext context)
        {
            // update section
            context.section = MaterialScriptSection.MSS_PROGRAM_REF;

            // Return TRUE because this must be followed by a {
            return true;
        }
        //-----------------------------------------------------------------------
        bool ParseShadowCasterVertexProgramRef(string param, ref MaterialScriptContext context)
        {
            // update section
            context.section = MaterialScriptSection.MSS_PROGRAM_REF;

            // Return TRUE because this must be followed by a {
            return true;
        }
        //-----------------------------------------------------------------------
        bool ParseShadowReceiverVertexProgramRef(string param, ref MaterialScriptContext context)
        {
            // update section
            context.section = MaterialScriptSection.MSS_PROGRAM_REF;

            // Return TRUE because this must be followed by a {
            return true;
        }
        //-----------------------------------------------------------------------
        bool ParseShadowReceiverFragmentProgramRef(string param, ref MaterialScriptContext context)
        {
            // update section
            context.section = MaterialScriptSection.MSS_PROGRAM_REF;

            // Return TRUE because this must be followed by a {
            return true;
        }
        //-----------------------------------------------------------------------
        bool ParseFragmentProgramRef(string param, ref MaterialScriptContext context)
        {
            // update section
            context.section = MaterialScriptSection.MSS_PROGRAM_REF;

            // Return TRUE because this must be followed by a {
            return true;
        }
        //-----------------------------------------------------------------------
        bool ParseTexture(string param, ref MaterialScriptContext context)
        {
            string[] vecparams = param.Split(" \t".ToCharArray());
            int numParams = vecparams.Length;
            if (numParams > 5)
            {
                logParseError("Invalid texture attribute - expected only up to 5 parameters.",
                    context);
            }
            TextureType tt = TextureType.TEX_TYPE_2D;
            int mipmaps = 1; // When passed to TextureManager::load, this means default to default number of mipmaps
            bool isAlpha = false;
            bool hwGamma = false;
            PixelFormat desiredFormat = PixelFormat.PF_UNKNOWN;
            for (int p = 1; p < numParams; ++p)
            {
                string tmp = vecparams[p].ToLower();
                if (tmp == "1d")
                {
                    tt = TextureType.TEX_TYPE_1D;
                }
                else if (tmp == "2d")
                {
                    tt = TextureType.TEX_TYPE_2D;
                }
                else if (tmp == "3d")
                {
                    tt = TextureType.TEX_TYPE_3D;
                }
                else if (tmp == "cubic")
                {
                    tt = TextureType.TEX_TYPE_CUBE_MAP;
                }
                else if (tmp == "unlimited")
                {
                    mipmaps = 0x7fffff;
                }
                else if (IsNumber(tmp))
                {
                    mipmaps = int.Parse(tmp);
                }
                else if (tmp == "alpha")
                {
                    isAlpha = true;
                }
                else if (tmp == "gamma")
                {
                    hwGamma = true;
                }
                //else if ((desiredFormat = PixelUtil::getFormatFromName(tmp, true)) != PF_UNKNOWN)
                //{
                //    // nothing to do here
                //}
                else
                {
                    logParseError("Invalid texture option - " + tmp + ".",
                    context);
                }
            }
            context.textureUnit.TextureName = vecparams[0];
            //context.textureUnit->setTextureName(vecparams[0], tt);
            //context.textureUnit->setNumMipmaps(mipmaps);
            //context.textureUnit->setIsAlpha(isAlpha);
            //context.textureUnit->setDesiredFormat(desiredFormat);
            //context.textureUnit->setHardwareGammaEnabled(hwGamma);
            return false;
        }
        //-----------------------------------------------------------------------
        bool ParseTextureUnitState(string param, ref MaterialScriptContext context)
        {
            // if params is a name then see if that texture unit exists
            // if not then log the warning and just move on to the next TU from current
            if (!string.IsNullOrEmpty(param) && (context.pass.NumTextureUnitStates > 0))
            {
                // specifying a TUS name in the script for a TU means that a specific TU is being requested
                // try to get the specific TU
                // if the index requested is not valid, just creat a new TU
                // find the TUS with name = params
                OgreTextureUnitState foundTUS = context.pass[param];
                if (foundTUS != null)
                {
                    context.stateLev = context.pass.GetTextureUnitStateIndex(foundTUS);
                }
                else
                {
                    // name was not found so a new TUS is needed
                    // position TUS level to the end index
                    // a new TUS will be created later on
                    context.stateLev = context.pass.NumTextureUnitStates;
                }
            }
            else
            {
                //Increase Texture Unit State level depth
                ++context.stateLev;
            }

            if (context.pass.NumTextureUnitStates > context.stateLev)
            {
                context.textureUnit = context.pass[context.stateLev];
            }
            else
            {
                // Create a new texture unit
                context.textureUnit = context.pass.CreateTextureUnitState();
                if (!string.IsNullOrEmpty(param))
                {
                    context.textureUnit.Name = param;
                }
            }
            // update section
            context.section = MaterialScriptSection.MSS_TEXTUREUNIT;

            // Return TRUE because this must be followed by a {
            return true;
        }
        //-----------------------------------------------------------------------
        bool ParsePass(string param, ref MaterialScriptContext context)
        {
            // if params is not empty then see if the pass name already exists
            if (!string.IsNullOrEmpty(param) && (context.technique.NumPasses > 0))
            {
                // find the pass with name = params
                OgrePass foundPass = context.technique[param];
                if (foundPass != null)
                {
                    context.passLev = foundPass.Index;
                }
                else
                {
                    // name was not found so a new pass is needed
                    // position pass level to the end index
                    // a new pass will be created later on
                    context.passLev = context.technique.NumPasses;
                }

            }
            else
            {
                //Increase pass level depth
                ++context.passLev;
            }

            if (context.technique.NumPasses > context.passLev)
            {
                context.pass = context.technique[context.passLev];
            }
            else
            {
                // Create a new pass
                context.pass = context.technique.CreatePass();
                if (!string.IsNullOrEmpty(param))
                {
                    context.pass.Name = param;
                }
            }

            // update section
            context.section = MaterialScriptSection.MSS_PASS;

            //
            return true;
        }
        //-----------------------------------------------------------------------
        bool ParseTechnique(string param, ref MaterialScriptContext context)
        {
            if (!string.IsNullOrEmpty(param) && context.material.NumTechniques > 0)
            {
                OgreTechnique tech = context.material[param];
                if (tech != null)
                {
                    context.techLev = context.material.GetTechniqueIndex(tech) - 1;
                }
                else
                {
                    context.techLev = context.material.NumTechniques;
                }
            }
            else
            {
                ++context.techLev;
            }

            // Create a new technique if it doesn't already exist
            if (context.material.NumTechniques > context.techLev)
            {
                context.technique = context.material[context.techLev];
            }
            else
            {
                context.technique = context.material.CreateTechnique();
                if (string.IsNullOrEmpty(param) == false)
                {
                    context.technique.Name = param;
                }
            }

            // update section
            context.section = MaterialScriptSection.MSS_TECHNIQUE;

            return true;
        }
        //-----------------------------------------------------------------------
        bool ParseMaterial(string param, ref MaterialScriptContext context)
        {
            string[] ps = param.Split(":".ToCharArray());
            OgreMaterial baseMaterial = null;
            if (ps.Length > 1)
            {
                string baseName = ps[1];
                baseMaterial = OgreMaterialManager.Instance.GetMaterial(baseName);
                if (baseMaterial == null)
                {
                    logParseError("parent material: " + ps[1] + " not found for new material:"
                    + ps[0], context);
                }
            }

            OgreMaterial material = new OgreMaterial(ps[0]);
            if (baseMaterial != null)
            {
                //clone
            }

            context.material = material;
            context.section = MaterialScriptSection.MSS_MATERIAL;

            return true;
        }
        //-----------------------------------------------------------------------
        bool ParseScriptLine(String line)
        {
            switch (mScriptContext.section)
            {
                case MaterialScriptSection.MSS_NONE:
                    if (line == "}")
                    {
                        logParseError("Unexpected terminating brace.", mScriptContext);
                        return false;
                    }
                    else
                    {
                        // find & invoke a parser
                        return InvokeParser(line, mRootAttribParsers);
                    }
                    break;
                case MaterialScriptSection.MSS_MATERIAL:
                    if (line == "}")
                    {
                        //Todo
                        // 贴图别名

                        mScriptContext.section = MaterialScriptSection.MSS_NONE;
                        mScriptContext.material = null;
                        //Reset all levels for next material
                        mScriptContext.passLev = -1;
                        mScriptContext.stateLev = -1;
                        mScriptContext.techLev = -1;
                    }
                    else
                    {
                        // find & invoke a parser
                        return InvokeParser(line, mMaterialAttribParsers);
                    }
                    break;
                case MaterialScriptSection.MSS_TECHNIQUE:
                    if (line == "}")
                    {
                        // End of technique
                        mScriptContext.section = MaterialScriptSection.MSS_MATERIAL;
                        mScriptContext.technique = null;
                        mScriptContext.passLev = -1;	//Reset pass level (yes, the pass level)
                    }
                    else
                    {
                        // find & invoke a parser
                        return InvokeParser(line, mTechniqueAttribParsers);
                    }
                    break;
                case MaterialScriptSection.MSS_PASS:
                    if (line == "}")
                    {
                        // End of pass
                        mScriptContext.section = MaterialScriptSection.MSS_TECHNIQUE;
                        mScriptContext.pass = null;
                        mScriptContext.stateLev = -1;	//Reset state level (yes, the state level)
                    }
                    else
                    {
                        // find & invoke a parser
                        return InvokeParser(line, mPassAttribParsers);
                    }
                    break;
                case MaterialScriptSection.MSS_TEXTUREUNIT:
                    if (line == "}")
                    {
                        // End of texture unit
                        mScriptContext.section = MaterialScriptSection.MSS_PASS;
                        mScriptContext.textureUnit = null;
                    }
                    else
                    {
                        // find & invoke a parser
                        return InvokeParser(line, mTextureUnitAttribParsers);
                    }
                    break;
                case MaterialScriptSection.MSS_TEXTURESOURCE:
                    if (line == "}")
                    {
                        mScriptContext.section = MaterialScriptSection.MSS_TEXTUREUNIT;
                    }
                    else
                    {
                        // custom texture parameter, use original line
                        ParseTextureCustomParameter(line, ref mScriptContext);
                    }
                    break;
                case MaterialScriptSection.MSS_PROGRAM_REF:
                    if (line == "}")
                    {
                        // End of program
                        mScriptContext.section = MaterialScriptSection.MSS_PASS;
                        //mScriptContext.program.setNull();
                    }
                    else
                    {
                        // find & invoke a parser
                        return InvokeParser(line, mProgramRefAttribParsers);
                    }
                    break;
                case MaterialScriptSection.MSS_PROGRAM:
                    // Program definitions are slightly different, they are deferred
                    // until all the information required is known
                    if (line == "}")
                    {
                        // End of program
                        //finishProgramDefinition();
                        mScriptContext.section = MaterialScriptSection.MSS_NONE;
                        //OGRE_DELETE_T(mScriptContext.programDef, MaterialScriptProgramDefinition, MEMCATEGORY_SCRIPTING);
                        //mScriptContext.defaultParamLines.clear();
                        //mScriptContext.programDef = NULL;
                    }
                    else
                    {
                        // find & invoke a parser
                        // do this manually because we want to call a custom
                        // routine when the parser is not found
                        // First, split line on first divisor only
                        string[] splitCmd = line.Split(" \t".ToCharArray());
                        // Find attribute parser
                        if (mProgramAttribParsers.ContainsKey(splitCmd[0]))
                        {
                            // custom parameter, use original line
                            ParseProgramCustomParameter(line, ref mScriptContext);
                        }
                        else
                        {
                            String cmd = splitCmd.Length >= 2 ? splitCmd[1] : "";
                            // Use parser with remainder
                            return mProgramAttribParsers[splitCmd[0]].Invoke(cmd, ref mScriptContext);
                        }

                    }
                    break;
                case MaterialScriptSection.MSS_DEFAULT_PARAMETERS:
                    if (line == "}")
                    {
                        // End of default parameters
                        mScriptContext.section = MaterialScriptSection.MSS_PROGRAM;
                    }
                    else
                    {
                        // Save default parameter lines up until we finalise the program
                        //mScriptContext.defaultParamLines.push_back(line);
                    }
                    break;
            };

            return false;
        }
        //-----------------------------------------------------------------------
        bool InvokeParser(String line, Dictionary<string, AttributeParse> parsers)
        {
            // First, split line on first divisor only
            string[] splitCmd = line.Split(" \t".ToCharArray());
            if (parsers.ContainsKey(splitCmd[0]) == false)
            {
                //logParseError("Unrecognised command: " + splitCmd[0], mScriptContext);
                return false;
            }
            else
            {
                String cmd = "";
                if (splitCmd.Length >= 2)
                    cmd = splitCmd[1];
                return parsers[splitCmd[0]].Invoke(cmd, ref mScriptContext);
            }
        }
        //-----------------------------------------------------------------------
        // Internal parser methods
        //-----------------------------------------------------------------------
        void logParseError(String error, MaterialScriptContext context)
        {
            return;//
            // log material name only if filename not specified
            if (string.IsNullOrEmpty(context.filename) && context.material != null)
            {
                GameViewer.Core.LogManager.Instance.LogMessage(
                    "Error in material " + context.material.Name +
                    " : " + error);
            }
            else
            {
                if (context.material != null)
                {
                    GameViewer.Core.LogManager.Instance.LogMessage(
                        "Error in material " + context.material.Name +
                        " at line " + context.lineNo.ToString() +
                        " of " + context.filename + ": " + error);
                }
                else
                {
                    GameViewer.Core.LogManager.Instance.LogMessage(
                        "Error at line " + context.lineNo.ToString() +
                        " of " + context.filename + ": " + error);
                }
            }
        }
        bool IsNumber(string text)
        {
            Regex regex = new Regex(@"^[-+]?[0-9]*\.?[0-9]+$");
            return regex.IsMatch(text);
        }
    }
}
